using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Collections;
using System.IO;

namespace PdfConverter
{
    public enum PdfConverterFormat
    {
        PDF = 0,
        PDF_A = 9,
        PDF_X = 10,
        TIFF = 5
    }

    /// <summary>
    /// Create by : TaGoH
    /// URL of the last version: http://www.codeproject.com/KB/cs/GhostScriptUseWithCSharp.aspx
    /// Description:
    /// Class to convert a pdf to an image using GhostScript DLL
    /// A big Credit for this code go to:Rangel Avulso
    /// I mainly create a better interface and refactor it to made it ready to use!
    /// </summary>
    /// <see cref="http://www.codeproject.com/KB/cs/GhostScriptUseWithCSharp.aspx"/>
    /// <seealso cref="http://www.hrangel.com.br/index.php/2006/12/04/converter-pdf-para-imagem-jpeg-em-c/"/>
    public class PdfConverter
    {
        #region Static
        private const string ghostScriptLibDirectoryPlaceHolder   = "<ghostScriptLibDirectory>";
        private const string windowsFontPathPlaceHoder                = "<windowsFontPath>";
        private const string inputPlaceHolder                                   = "<input>";
        private const string outputPlaceHolder                                 = "<output>";
        private const string xResolutionPlaceHolder                        = "<xResolution>";
        private const string yResolutionPlaceHolder                        = "<xResolution>";


        //predefined parameters
        private static string[] GS_ConvertPDFParameters = new string[]
        {
            "-q",
            "-dNOPAUSE",
            "-dBATCH",
            "-sFONTPATH=" + windowsFontPathPlaceHoder,
            "-sDEVICE=pdfwrite",
            "-dPDFSETTINGS=/default",
            "-dCompatibilityLevel=1.4",
            "-r" + xResolutionPlaceHolder + "x" + yResolutionPlaceHolder,
            "-dProcessColorModel=/DeviceCMYK",
            "-dAutoRotatePages=/PageByPage",
            "-dCompressPages=true",
            "-dEmbedAllFonts=true",
            "-dSubsetFonts=true",
            "-dMaxSubsetPct=100",
            "-dConvertCMYKImagesToRGB=false",
            "-sOutputFile=" + outputPlaceHolder,
            "-dEncodeColorImages=true",
            "-dAutoFilterColorImages=true",
            "-dEncodeGrayImages=true",
            "-dAutoFilterGrayImages=true",
            "-dEncodeMonoImages=true",
            "-dMonoImageFilter=/CCITTFaxEncode",
            "-dDownsampleMonoImages=false",
            "-dPreserveOverprintSettings=true",
            "-dUCRandBGInfo=/Preserve",
            "-dUseFlateCompression=true",
            "-dParseDSCCommentsForDocInfo=true",
            "-dParseDSCComments=true",
            "-dOPM=0",
            "-dOffOptimizations=0",
            "-dLockDistillerParams=false",
            "-dGrayImageDepth=-1",
            "-dASCII85EncodePages=false",
            "-dDefaultRenderingIntent=/Default",
            "-dTransferFunctionInfo=/Preserve",
            "-dPreserveHalftoneInfo=false",
            "-dDetectBlends=true",
            inputPlaceHolder
        };
        private static string[] GS_ConvertPDFAParameters = new string[]
        {
            "-I" + ghostScriptLibDirectoryPlaceHolder,
            "-q",
            "-dNOPAUSE",
            "-dBATCH",
            "-sFONTPATH=" + windowsFontPathPlaceHoder,
            "-sDEVICE=pdfwrite",
            "-dPDFSETTINGS=/default",
            "-dCompatibilityLevel=1.4",
            "-dNOOUTERSAVE",
            "-dUseCIEColor",
            "-r" + xResolutionPlaceHolder + "x" + yResolutionPlaceHolder,
            "-sProcessColorModel=DeviceCMYK",
            "-dAutoRotatePages=/PageByPage",
            "-dCompressPages=true",
            "-dEmbedAllFonts=true",
            "-dSubsetFonts=true",
            "-dMaxSubsetPct=100",
            "-dConvertCMYKImagesToRGB=false",
            "-sOutputFile=" + outputPlaceHolder,
            "-dEncodeColorImages=true",
            "-dAutoFilterColorImages=true",
            "-dEncodeGrayImages=true",
            "-dAutoFilterGrayImages=true",
            "-dEncodeMonoImages=true",
            "-dMonoImageFilter=/CCITTFaxEncode",
            "-dDownsampleMonoImages=false",
            "-dPreserveOverprintSettings=true",
            "-dUCRandBGInfo=/Preserve",
            "-dUseFlateCompression=true",
            "-dParseDSCCommentsForDocInfo=true",
            "-dParseDSCComments=true",
            "-dOPM=1",
            "-dOffOptimizations=0",
            "-dLockDistillerParams=false",
            "-dGrayImageDepth=-1",
            "-dASCII85EncodePages=false",
            "-dDefaultRenderingIntent=/Default",
            "-dTransferFunctionInfo=/Preserve",
            "-dPreserveHalftoneInfo=false",
            "-dDetectBlends=true",
            "-dPDFA",
            "-f",
            ghostScriptLibDirectoryPlaceHolder + "pdfa_def.ps",
            "-f",
            inputPlaceHolder
        };
        private static string[] GS_ConvertPDFXParameters = new string[]
        {
            "-I" + ghostScriptLibDirectoryPlaceHolder,
            "-q",
            "-dNOPAUSE",
            "-dBATCH",
            "-sFONTPATH="+windowsFontPathPlaceHoder,
            "-sDEVICE=pdfwrite",
            "-dPDFSETTINGS=/default",
            "-dCompatibilityLevel=1.4",
            "-r" + xResolutionPlaceHolder + "x" + yResolutionPlaceHolder,
            "-dProcessColorModel=/DeviceCMYK",
            "-dAutoRotatePages=/PageByPage",
            "-dCompressPages=true",
            "-dEmbedAllFonts=true",
            "-dSubsetFonts=true",
            "-dMaxSubsetPct=100",
            "-dConvertCMYKImagesToRGB=false",
            "-sOutputFile=" + outputPlaceHolder,
            "-dEncodeColorImages=true",
            "-dAutoFilterColorImages=true",
            "-dEncodeGrayImages=true",
            "-dAutoFilterGrayImages=true",
            "-dEncodeMonoImages=true",
            "-dMonoImageFilter=/CCITTFaxEncode",
            "-dDownsampleMonoImages=false",
            "-dPreserveOverprintSettings=true",
            "-dUCRandBGInfo=/Preserve",
            "-dUseFlateCompression=true",
            "-dParseDSCCommentsForDocInfo=true",
            "-dParseDSCComments=true",
            "-dOPM=0",
            "-dOffOptimizations=0",
            "-dLockDistillerParams=false",
            "-dGrayImageDepth=-1",
            "-dASCII85EncodePages=false",
            "-dDefaultRenderingIntent=/Default",
            "-dTransferFunctionInfo=/Preserve",
            "-dPreserveHalftoneInfo=false",
            "-dDetectBlends=true",
            "-dPDFX",
            "-f",
            ghostScriptLibDirectoryPlaceHolder + "pdfx_def.ps",
            "-f",
            inputPlaceHolder
        };
        private static string[] GS_ConvertTIFFParameters = new string[]
        {
            "pdf2img",
            "-dNOPAUSE",
			"-dBATCH",
			"-dSAFER",
			"-sDEVICE=tiff24nc",
			"-r" + xResolutionPlaceHolder + "x" + yResolutionPlaceHolder,
			"-sOutputFile=" + outputPlaceHolder,
			inputPlaceHolder
        };

        private static Dictionary<PdfConverterFormat, string[]> formatParameters =
                                            new Dictionary<PdfConverterFormat, string[]>();

        static PdfConverter()
        {
            formatParameters.Add(PdfConverterFormat.PDF, GS_ConvertPDFParameters);
            formatParameters.Add(PdfConverterFormat.PDF_A, GS_ConvertPDFAParameters);
            formatParameters.Add(PdfConverterFormat.PDF_X, GS_ConvertPDFXParameters);
            formatParameters.Add(PdfConverterFormat.TIFF, GS_ConvertTIFFParameters);
        }
        #endregion
        #region Windows Import
        /// <summary>Needed to copy memory from one location to another, used to fill the struct</summary>
        /// <param name="Destination"></param>
        /// <param name="Source"></param>
        /// <param name="Length"></param>
        [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")]
        static extern void CopyMemory(IntPtr Destination, IntPtr Source, uint Length);
        #endregion
        #region GhostScript Import

        /// <summary>Create a new instance of Ghostscript. This instance is passed to most other gsapi functions. The caller_handle will be provided to callback functions.
        ///  At this stage, Ghostscript supports only one instance. </summary>
        /// <param name="pinstance"></param>
        /// <param name="caller_handle"></param>
        /// <returns></returns>
        [DllImport("gsdll32.dll", EntryPoint = "gsapi_new_instance")]
        private static extern int gsapi_new_instance(out IntPtr pinstance, IntPtr caller_handle);

        /// <summary>This is the important function that will perform the conversion</summary>
        /// <param name="instance"></param>
        /// <param name="argc"></param>
        /// <param name="argv"></param>
        /// <returns></returns>
        [DllImport("gsdll32.dll", EntryPoint = "gsapi_init_with_args")]
        private static extern int gsapi_init_with_args(IntPtr instance, int argc, IntPtr argv);
        /// <summary>
        /// Exit the interpreter. This must be called on shutdown if gsapi_init_with_args() has been called, and just before gsapi_delete_instance(). 
        /// </summary>
        /// <param name="instance"></param>
        /// <returns></returns>
        [DllImport("gsdll32.dll", EntryPoint = "gsapi_exit")]
        private static extern int gsapi_exit(IntPtr instance);

        /// <summary>
        /// Destroy an instance of Ghostscript. Before you call this, Ghostscript must have finished. If Ghostscript has been initialised, you must call gsapi_exit before gsapi_delete_instance. 
        /// </summary>
        /// <param name="instance"></param>
        [DllImport("gsdll32.dll", EntryPoint = "gsapi_delete_instance")]
        private static extern void gsapi_delete_instance(IntPtr instance);
        /// <summary>Get info about the version of Ghostscript i'm using</summary>
        /// <param name="pGSRevisionInfo"></param>
        /// <param name="intLen"></param>
        /// <returns></returns>
        [DllImport("gsdll32.dll", EntryPoint = "gsapi_revision")]
        private static extern int gsapi_revision(ref GS_Revision pGSRevisionInfo, int intLen);
        /// <summary>Use a different I/O</summary>
        /// <param name="lngGSInstance"></param>
        /// <param name="gsdll_stdin">Function that menage the Standard INPUT</param>
        /// <param name="gsdll_stdout">Function that menage the Standard OUTPUT</param>
        /// <param name="gsdll_stderr">Function that menage the Standard ERROR output</param>
        /// <returns></returns>
        [DllImport("gsdll32.dll", EntryPoint = "gsapi_set_stdio")]
        private static extern int gsapi_set_stdio(IntPtr lngGSInstance, StdioCallBack gsdll_stdin, StdioCallBack gsdll_stdout, StdioCallBack gsdll_stderr);

        #endregion
        #region Const
        const int e_Quit = -101;
        const int e_NeedInput = -106;
        #endregion
        #region Variables
        /// <summary>In how many thread i should perform the conversion</summary>
        /// <remarks>This is a Major innovation since 8.63 NEVER use it with previous version!</remarks>
        private int _iRenderingThreads = -1;

        /// <summary>In how many thread i should perform the conversion</summary>
        /// <remarks>This is a Major innovation since 8.63 NEVER use it with previous version!</remarks>
        /// <value>Set it to 0 made the program set it to Environment.ProcessorCount HT machine could want to perform a check for this..</value>
        public int RenderingThreads
        {
            get { return _iRenderingThreads; }
            set
            {
                if (value <= 0)
                    _iRenderingThreads = Environment.ProcessorCount;
                else
                    _iRenderingThreads = value;
            }
        }
        private IntPtr _objHandle;

        #endregion

        #region Init
        public PdfConverter(IntPtr objHandle)
        {
            _objHandle = objHandle;
        }

        public PdfConverter()
        {
            _objHandle = IntPtr.Zero;
        }
        #endregion

        #region Convert

        /// <summary>Convert a single file!</summary>
        /// <param name="inputFile">The PDF document to convert</param>
        /// <param name="outputFile">The image file that will be created</param>
        /// <remarks>You must pass all the parameter for the conversion
        /// as Proprieties of this class</remarks>
        /// <returns>True if the conversion succed!</returns>
        private bool Convert(string inputFile, string outputFile, string[] args)
        {
            #region Check Input
            //Avoid to work when the file doesn't exist
            if (string.IsNullOrEmpty(inputFile))
            {
                throw new ArgumentNullException("inputFile");
            }
            if (!System.IO.File.Exists(inputFile))
            {
                throw new ArgumentException(string.Format("The file :'{0}' doesn't exist", inputFile), "inputFile");
            }

            #endregion
            #region Variables
            int intReturn, intCounter, intElementCount;
            //The pointer to the current istance of the dll
            IntPtr intGSInstanceHandle;
            object[] aAnsiArgs;
            IntPtr[] aPtrArgs;
            GCHandle[] aGCHandle;
            IntPtr callerHandle, intptrArgs;
            GCHandle gchandleArgs;
            #endregion

            #region Convert Unicode strings to null terminated ANSI byte arrays
            // Convert the Unicode strings to null terminated ANSI byte arrays
            // then get pointers to the byte arrays.
            intElementCount = args.Length;
            aAnsiArgs = new object[intElementCount];
            aPtrArgs = new IntPtr[intElementCount];
            aGCHandle = new GCHandle[intElementCount];
            //Convert the parameters
            for (intCounter = 0; intCounter < intElementCount; intCounter++)
            {
                aAnsiArgs[intCounter] = StringToAnsiZ(args[intCounter]);
                aGCHandle[intCounter] = GCHandle.Alloc(aAnsiArgs[intCounter], GCHandleType.Pinned);
                aPtrArgs[intCounter] = aGCHandle[intCounter].AddrOfPinnedObject();
            }
            gchandleArgs = GCHandle.Alloc(aPtrArgs, GCHandleType.Pinned);
            intptrArgs = gchandleArgs.AddrOfPinnedObject();
            #endregion
            #region Create a new istance of the library!
            intReturn = -1;
            try
            {
                intReturn = gsapi_new_instance(out intGSInstanceHandle, _objHandle);
                //Be sure that we create an istance!
                if (intReturn < 0)
                {
                    ClearParameters(ref aGCHandle, ref gchandleArgs);
                    throw new ApplicationException("I can't create a new istance of Ghostscript please verify no other istance are running!");
                }
            }
            catch (DllNotFoundException)
            {
                //in this case the dll we're using isn't the dll we expect
                ClearParameters(ref aGCHandle, ref gchandleArgs);
                throw new ApplicationException("The gsdll32.dll wasn't found in default dlls search path " +
                        "or is not in correct version (doesn't expose the required methods). Please download " +
                        "the version 8.64 from the original website");
            }
            callerHandle = IntPtr.Zero;//remove unwanter handler
            #endregion
            intReturn = -1;//if nothing change it there is an error!
            //Ok now is time to call the interesting method
            try 
            { 
                intReturn = gsapi_init_with_args(intGSInstanceHandle, intElementCount, intptrArgs); 
            }
            catch (Exception ex)
            {
                throw new ApplicationException(ex.Message, ex);
            }
            finally//No matter what happen i MUST close the istance!
            {   //free all the memory
                ClearParameters(ref aGCHandle, ref gchandleArgs);
                gsapi_exit(intGSInstanceHandle);//Close the istance
                gsapi_delete_instance(intGSInstanceHandle);//delete it
            }
            //Conversion was successfull if return code was 0 or e_Quit
            return (intReturn == 0) | (intReturn == e_Quit);//e_Quit = -101
        }

        public bool ConvertByFormat(string gsLibDir, string fontPath, string inputFile, string outputFile, 
                                                 int xRes, int yRes, int format)
        {
            string[] parameters = (string[]) formatParameters[(PdfConverterFormat)format].Clone();

            for (int i = 0; i < parameters.Length; ++i)
            {
                string para = parameters[i];
                para = para.Replace(ghostScriptLibDirectoryPlaceHolder, gsLibDir);
                para = para.Replace(windowsFontPathPlaceHoder, fontPath);
                para = para.Replace(inputPlaceHolder, inputFile);
                para = para.Replace(outputPlaceHolder, outputFile);
                para = para.Replace(xResolutionPlaceHolder, xRes.ToString());
                para = para.Replace(xResolutionPlaceHolder, yRes.ToString());

                parameters[i] = para;
            }


            RenderingThreads = Environment.ProcessorCount;
            return Convert(inputFile, outputFile, parameters);
        }

        /// <summary>Remove the memory allocated</summary>
        /// <param name="aGCHandle"></param>
        /// <param name="gchandleArgs"></param>
        private void ClearParameters(ref GCHandle[] aGCHandle, ref GCHandle gchandleArgs)
        {
            for (int intCounter = 0; intCounter < aGCHandle.Length; intCounter++)
                aGCHandle[intCounter].Free();
            gchandleArgs.Free();
        }
        #endregion

        #region Accessory Functions


        /// <summary>
        /// Convert a Unicode string to a null terminated Ansi string for Ghostscript.
        /// The result is stored in a byte array
        /// </summary>
        /// <param name="str">The parameter i want to convert</param>
        /// <returns>the byte array that contain the string</returns>
        private static byte[] StringToAnsiZ(string str)
        {
            // Later you will need to convert
            // this byte array to a pointer with
            // GCHandle.Alloc(XXXX, GCHandleType.Pinned)
            // and GSHandle.AddrOfPinnedObject()
            //int intElementCount,intCounter;

            //This with Encoding.Default should work also with Chineese Japaneese
            //Thanks to tchu_2000 I18N related patch
            if (str == null) str = String.Empty;
            return Encoding.Default.GetBytes(str);
        }

        /// <summary>Convert a Pointer to a string to a real string</summary>
        /// <param name="strz">the pointer to the string in memory</param>
        /// <returns>The string</returns>
        public static string AnsiZtoString(IntPtr strz)
        {
            if (strz != IntPtr.Zero)
                return Marshal.PtrToStringAnsi(strz);
            else
                return string.Empty;
        }
        #endregion
        #region Menage Standard Input & Standard Output
        public int gsdll_stdin(IntPtr intGSInstanceHandle, IntPtr strz, int intBytes)
        {
            // This is dumb code that reads one byte at a time
            // Ghostscript doesn't mind this, it is just very slow
            if (intBytes == 0)
                return 0;
            else
            {
                int ich = Console.Read();
                if (ich == -1)
                    return 0; // EOF
                else
                {
                    byte bch = (byte)ich;
                    GCHandle gcByte = GCHandle.Alloc(bch, GCHandleType.Pinned);
                    IntPtr ptrByte = gcByte.AddrOfPinnedObject();
                    CopyMemory(strz, ptrByte, 1);
                    ptrByte = IntPtr.Zero;
                    gcByte.Free();
                    return 1;
                }
            }
        }

        public int gsdll_stdout(IntPtr intGSInstanceHandle, IntPtr strz, int intBytes)
        {
            if (intBytes > 0)
            {
                Console.Write(Marshal.PtrToStringAnsi(strz));
            }
            return 0;
        }

        public int gsdll_stderr(IntPtr intGSInstanceHandle, IntPtr strz, int intBytes)
        {
            //return gsdll_stdout(intGSInstanceHandle, strz, intBytes);
            Console.Write(Marshal.PtrToStringAnsi(strz));
            return intBytes;
        }
        #endregion
        #region Menage Revision
        public GhostScriptRevision GetRevision()
        {
            // Check revision number of Ghostscript
            int intReturn;
            GS_Revision udtGSRevInfo = new GS_Revision();
            GhostScriptRevision output;
            GCHandle gcRevision;
            gcRevision = GCHandle.Alloc(udtGSRevInfo, GCHandleType.Pinned);
            intReturn = gsapi_revision(ref udtGSRevInfo, 16);
            output.intRevision = udtGSRevInfo.intRevision;
            output.intRevisionDate = udtGSRevInfo.intRevisionDate;
            output.ProductInformation = AnsiZtoString(udtGSRevInfo.strProduct);
            output.CopyrightInformations = AnsiZtoString(udtGSRevInfo.strCopyright);
            gcRevision.Free();
            return output;
        }
        #endregion
    }


    /// <summary>Delegate used by Ghostscript to perform I/O operations</summary>
    /// <param name="handle"></param>
    /// <param name="strptr"></param>
    /// <param name="count"></param>
    /// <returns></returns>
    public delegate int StdioCallBack(IntPtr handle, IntPtr strptr, int count);
    /// <summary>This struct is filled with the information of the version of this ghostscript</summary>
    /// <remarks>Have the layout defined cuz i will fill it with a kernel copy memory</remarks>
    [StructLayout(LayoutKind.Sequential)]
    struct GS_Revision
    {
        public IntPtr strProduct;
        public IntPtr strCopyright;
        public int intRevision;
        public int intRevisionDate;
    }

    public struct GhostScriptRevision
    {
        public string ProductInformation;
        public string CopyrightInformations;
        public int intRevision;
        public int intRevisionDate;
    }
}
